2.3 常量

常量表示运行时恒定不可改变的值,通常是一些字面量。使用常量就可用一个易于阅读理解的标识符号来代替“魔法数字”,也使得在调整常量值时,无须修改所有引用代码。

常量值必须是编译期可确定的字符、字符串、数字或布尔值。可指定常量类型,或由编译器通过初始化值推断,不支持C/C++数字类型后缀。

const x,y int=123,0x22
const s= "hello,world!" 
const c= ''              //rune(unicode code point) 
  
const( 
   i,f=1,0.123         //int,float64(默认) 
   b   =false
)

可在函数代码块中定义常量,不曾使用的常量不会引发编译错误。

func main() { 
   const x=123
   println(x) 
  
   const y=1.23          // 未使用,不会引发编译错误 
  
    { 
       const x= "abc"            // 在不同作用域定义同名常量 
       println(x) 
    } 
}
 

输出:

123 abc

如果显式指定类型,必须确保常量左右值类型一致,需要时可做显式转换。右值不能超出常量类型取值范围,否则会引发溢出错误。

const( 
   x,y int   =99, -999
   b  byte  =byte(x)       //x被指定为int类型,须显式转换为byte类型 
   n          =uint8(y)       // 错误:constant-999 overflows uint8
)

常量值也可以是某些编译器能计算出结果的表达式,如unsafe.Sizeof、len、cap等。

import"unsafe" 
  
const( 
   ptrSize=unsafe.Sizeof(uintptr(0)) 
   strSize=len("hello,world!") 
)
 

在常量组中如不指定类型和初始化值,则与上一行非空常量右值(表达式文本)相同

import "fmt" 
  
func main() { 
   const( 
       x uint16=120
       y               // 与上一行x类型、右值相同 
       s  = "abc"                
       z               // 与s类型、右值相同 
    ) 
  
   fmt.Printf("%T, %v\n",y,y)       // 输出类型和值 
   fmt.Printf("%T, %v\n",z,z) 
}
 

输出:

uint16,120 string,abc

枚举

Go并没有明确意义上的enum定义,不过可借助iota标识符实现一组自增常量值来实现枚举类型。

const( 
   x  = iota  //0
   y     //1
   z     //2
) 
  
const( 
    _   = iota      //0
   KB = 1 << (10*iota) //1<< (10*1) 
   MB        //1<< (10*2) 
   GB        //1<< (10*3) 
)

自增作用范围为常量组。可在多常量定义中使用多个iota,它们各自单独计数,只须确保组中每行常量的列数量相同即可

const( 
    _, _ = iota,iota*10   //0,0*10
   a,b           //1,1*10
   c,d           //2,2*10
)

如中断iota自增,则必须显式恢复。且后续自增值按行序递增,而非C enum那般按上一取值递增。

const( 
   a  =iota          //0
   b             //1
   c  =100           //100
   d             //100(与上一行常量右值表达式相同) 
   e  =iota          //4(恢复iota自增,计数包括c、d) 
   f             //5
)

自增默认数据类型为int,可显式指定类型。

const( 
   a          =iota      //int
   b float32 = iota    //float32
   c          =iota      //int(如不显式指定iota,则与b数据类型相同) 
)

在实际编码中,建议用自定义类型实现用途明确的枚举类型。但这并不能将取值范围限定在预定义的枚举值内。

type color byte   // 自定义类型 
  
const( 
   black color=iota       // 指定常量类型 
   red
   blue
) 
  
func test(c color) { 
   println(c) 
} 
  
func main() { 
   test(red) 
   test(100)           //100并未超出color/byte类型取值范围 
  
   x:=2     
   test(x)         // 错误:cannot use x(type int)as type color in argument to test
}

展开

常量除“只读”外,和变量究竟有什么不同?

var x=0x100
const y=0x200
  
func main() { 
   println(&x,x) 
   println(&y,y)       // 错误:cannot take the address of y
}

不同于变量在运行期分配存储内存(非优化状态),常量通常会被编译器在预处理阶段直接展开,作为指令数据使用

const y=0x200
  
func main() { 
   println(y) 
}
 

输出:

$go build&&go tool objdump-s"main\.main"test
  
TEXT main.main(SB)test.go
   MOVQ$0x200,0(SP)        // 将常量值作为指令数据展开 
   CALL runtime.printint(SB)

数字常量不会分配存储空间,无须像变量那样通过内存寻址来取值,因此无法获取地址

鉴于Go当前对动态库的支持还不完善,是否存在“常量陷阱”问题,尚有待观察。

提到常量展开,我们还须回头看看常量的两种状态对编译器的影响。

const x=100         // 无类型声明的常量 
const y byte=x       // 直接展开x,相当于const y byte=100
  
const a int=100      // 显式指定常量类型,编译器会做强类型检查 
const b byte=a       // 错误:cannot use a(type int)as type byte in const initializer